﻿using System;
using System.IO;
using System.Collections.Generic;
using DatabaseStuff;

namespace DatabaseStuff.Basic
{
    /// <summary>
    /// Represents a plaintext source for database information
    /// </summary>
    public abstract class TextSource: IDBSource
    {
        private TextStorageFormatSettings settings;

        internal TextSource(TextStorageFormatSettings _settings)
        {
            settings = _settings;
        }

        public TextSource()
        {
            settings = TextStorageFormatSettings.Default;
        }

        IDatabase DatabaseStuff.IDBSource.ReadData()
        {
            return ReadData();
        }

        public Database ReadData()
        {
            Database result = new Database();
            Table t;
            Schema s;
            TextReader tmp;
            IEnumerable<string> names;

            tmp = GetTableNamesReader();
            try
            { names = ImportTableNames(tmp); }
            finally
            { tmp.Close(); }

            foreach (string name in names)
            {
                tmp = GetSchemaReader(name);
                try
                { s = ImportSchema(tmp); }
                finally
                { tmp.Close(); }

                tmp = GetRecordsReader(name);
                try
                { t = ImportTable(tmp, name, s); }
                finally
                { tmp.Close(); }

                result.GetTables().Add(t);
            }

            return result;
        }

        /// <summary>
        /// Obtain a TextReader that can be used to read the schema for the table
        /// with the specified name
        /// </summary>
        /// <param name="table_name">The name of the table</param>
        /// <returns>a TextReader that can be used to read the schema for the table
        /// with the specified name</returns>
        protected abstract TextReader GetSchemaReader(string table_name);

        /// <summary>
        /// Obtain a TextReader that can be used to read the records for the table
        /// with the specified name
        /// </summary>
        /// <param name="table_name">The name of the table</param>
        /// <returns>a TextReader that can be used to read the records for the table
        /// with the specified name</returns>
        protected abstract TextReader GetRecordsReader(string table_name);

        /// <summary>
        /// Obtain a TextReader that can be used to read the names of all tables
        /// in the database
        /// </summary>
        /// <returns>a TextReader that can be used to read the names of all tables
        /// in the database</returns>
        protected abstract TextReader GetTableNamesReader();

        private Table ImportTable(TextReader records_reader, string tableName, Schema tableSchema)
        {
            VerifyReadability(records_reader);

            Table result = new Table(tableName,tableSchema);

            string ln;
            string[] splitLine;
            int iterator;
            Record curRecord;
            Field curField;
            DBData dat;

            while (records_reader.Peek() != -1)
            {
                ln = records_reader.ReadLine().Trim();

                if (String.IsNullOrEmpty(ln))
                    continue;

                if (ln.Equals(settings.SignalEOF))
                    break;

                splitLine = ln.Split(settings.Delimiter);
                if (splitLine.Length != tableSchema.NumFields)
                    throw new FormatException("Record entry did not contain data for the expected number of fields");

                curRecord = new Record(result);

                for (iterator = 0; iterator < tableSchema.NumFields; iterator++)
                {
                    curField = tableSchema.Fields[iterator];

                    try
                    {
                        dat = new DBData(curField, curRecord, DBDataType.ParseObject(splitLine[iterator], curField.Type));
                    }
                    catch (FormatException ex)
                    {
                        throw new FormatException("Failed to parse a typed data value from source. Expected Type: " + curField.Type, ex);
                    }

                    curRecord.Contents.Add(dat);
                }

                result.Records.Add(curRecord);
            }

            return result;
        }

        /// <summary>
        /// Import a schema using the specified TextReader.
        /// The caller of this method is responsible for opening and closing
        /// the TextReader as appropriate.
        /// </summary>
        private Schema ImportSchema(TextReader schema_reader)
        {
            IList<Field> field_intermediary = new List<Field>();

            VerifyReadability(schema_reader);

            Field f, pkField = null;
            string ln = String.Empty;
            string[] data;

            while (schema_reader.Peek() != -1)
            {
                ln = schema_reader.ReadLine().Trim();

                if (String.IsNullOrEmpty(ln))
                    continue;

                if (ln.Equals(settings.SignalEOF))
                    break;

                data = ln.Split(settings.Delimiter);

                f = ImportField(data);

                if (f.IsPrimaryKey)
                {
                    if (pkField == null)
                        pkField = f;
                    else
                        throw new FormatException("More than one field marked as primary key");
                }

                field_intermediary.Add(f);
            }

            if (pkField == null)
                throw new FormatException("No field marked as primary key");

            return new Schema(field_intermediary, pkField);
        }

        private Field ImportField(string[] data)
        {
            DBDataType type;
            bool isPrimaryKey;
            Field result;

            if (data.Length == 3
                && !String.IsNullOrEmpty(data[0])
                && DBDataType.TryParse(data[1], out type)
                && Boolean.TryParse(data[2], out isPrimaryKey))
            { result = new Field(data[0], isPrimaryKey, type); }

            else
                throw new FormatException("Improperly formatted field entry");

            return result;
        }

        private static void VerifyReadability(TextReader reader)
        {
            try
            {
                reader.Peek();
            }
            catch (NullReferenceException ex)
            {
                throw new ArgumentNullException("The specified TextReader reference is null", ex);
            }
            catch (ObjectDisposedException ex)
            {
                throw new ArgumentException("The specified TextReader appears to be closed.", ex);
            }
            catch (IOException ex)
            {
                throw new IOException("An IOException occurred while verifying the usability of the specified TextReader", ex);
            }
        }

        /// <summary>
        /// Import the table names using the specified TextReader.
        /// The caller of this method is responsible for opening and closing
        /// the TextReader as appropriate.
        /// </summary>
        /// <param name="tables_reader">The TextReader with which to acquire
        /// the table names.</param>
        private ICollection<string> ImportTableNames(TextReader tables_reader)
        {
            ICollection<string> table_names = new HashSet<string>();
            VerifyReadability(tables_reader);

            string ln = String.Empty;

            while (tables_reader.Peek() != -1)
            {
                try
                {
                    ln = tables_reader.ReadLine().Trim();
                }
                catch (IOException ex)
                {
                    throw new IOException("An IOException occurred while trying to read data using the specified TextReader", ex);
                }

                if (String.IsNullOrEmpty(ln))
                    continue;

                if (ln.Equals(settings.SignalEOF))
                    break;

                if (!String.IsNullOrEmpty(ln))
                    table_names.Add(ln);
            }

            return table_names;
        }
    }
}
